﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.Net;
using BlackBBS.Infrastructure.Logging;
using System.IO;
using BlackBBS.Helpers;

namespace BlackBBS.Infrastructure.TelnetProtocol
{
    public class TelnetClient : IDisposable
    {
        public const string MessageTemplate = "\r\n\x1B[1;34;47m**\x1B[40m {0} \x1B[1;34;47m**\x1B[0m\r\n";
        public const string ErrorMessageTemplate = "\r\n\x1B[1;31;40m**\x1B[41m {0} \x1B[1;31;40m**\x1B[0m\r\n";

        private string _keyInputMask = string.Empty;        // String to echo versus sending key input (ie password entry)
        private Socket _socket;						        // Connection to the client
        private byte[] _socketBuffer = new byte[32767];		// Receive data buffer
        private string _workingData = string.Empty;         // Inbound string buffer since last WaitFor return result 

        /// <summary>
        /// This is which node the telnet client session is for
        /// </summary>
        public int Node { get; set; }

        private event TelnetEventHandler _onReceived;
        private event TelnetEventHandler _onSent;
        private event TelnetEventHandler _onDisconnected;
        private event TelnetErrorEventHandler _onException;

        private object _onReceivedLock = new object();
        private object _onDisconnectedLock = new object();
        private object _onExceptionLock = new object();
        private object _onSentLock = new object();

        /// <summary>
        /// Event raised when Data recieved 
        /// </summary>
        public event TelnetEventHandler OnReceived
        {
            add
            {
                lock (this._onReceivedLock)
                {
                    this._onReceived += value;
                }
            }
            remove
            {
                lock (this._onReceivedLock)
                {
                    this._onReceived -= value;
                }
            }
        }

        /// <summary>
        /// Event raised when socket disconnected
        /// </summary>
        public event TelnetEventHandler OnDisconnect
        {
            add
            {
                lock (this._onDisconnectedLock)
                {
                    this._onDisconnected += value;
                }
            }
            remove
            {
                lock (this._onDisconnectedLock)
                {
                    this._onDisconnected -= value;
                }
            }
        }

        /// <summary>
        /// Event raised when an exception occured
        /// </summary>
        public event TelnetErrorEventHandler OnException
        {
            add
            {
                lock (this._onExceptionLock)
                {
                    this._onException += value;
                }
            }
            remove
            {
                lock (this._onExceptionLock)
                {
                    this._onException -= value;
                }
            }
        }


        /// <summary>
        /// Event raised after socket sent data
        /// </summary>
        public event TelnetEventHandler OnSent
        {
            add
            {
                lock (this._onSentLock)
                {
                    this._onSent += value;
                }
            }
            remove
            {
                lock (this._onSentLock)
                {
                    this._onSent -= value;
                }
            }
        }

        public Socket Socket
        {
            get { return this._socket; }
        }

        /// <summary>
        /// Timeout to wait for a response
        /// </summary>
        public int Timeout { get; set; }

        public TelnetClient(Socket socket, int node = 0)
        {
            this._socket = socket;
            this.Node = node;
            this.Timeout = 180;
        }

        public bool Connect()
        {
            try
            {
                AsyncCallback recieveData = new AsyncCallback(this.OnRecievedData);
                this._socket.BeginReceive(this._socketBuffer, 0, this._socketBuffer.Length, SocketFlags.None, recieveData, this._socket);
                return true;
            }
            catch (Exception ex)
            {
                this.RaiseError(ex, string.Format("Failed To Connect To Socket LocalEndPoint [{0}]", this._socket.LocalEndPoint));
                return false;
            }            
        }

        /// <summary>
        /// Sends a string to the socket
        /// </summary>
        /// <param name="Message">The message to send to the connected client</param>
        /// <param name="SuppressCarriageReturn">True if you do not want to end the message with a carriage return</param>
        public void SendString(string message, bool suppressCarriageReturn = true)
        {
            if (!suppressCarriageReturn)
            {
                message = message + ANSICharacterCodes.KeyCrLf;
            }
            this.DoSend(message);
        }

        /// <summary>
        /// Send the Clear Screen ANSI command
        /// </summary>
        /// <param name="socket"></param>
        public void ClearScreen()
        {
            SendString(ANSICharacterCodes.ANSIClearScreen);
        }

        /// <summary>
        /// Sends a message then waits for the result
        /// </summary>
        public string SendAndWait(string message, string waitfor, bool suppressCarriageReturn = false)
        {
            this.SendString(message, suppressCarriageReturn);
            return this.WaitFor(waitfor);
        }

        public string SendAndWaitForEnter(string message)
        {
            this.SendString(message);
            return this.WaitForEnter();
        }

        public void SendAndWaitForAnyKey(string message)
        {
            this.SendString(message);
            this.WaitForAnykey();
        }

        /// <summary>
        /// Defaults to sending the ANS file from the ans folder under the app path
        /// </summary>
        /// <param name="filename"></param>
        public void SendFile(string filename)
        {
            FileInfo file = new FileInfo(filename);
            if (this.ValidateFileExists(file))
            {
                if (!this._socket.Connected)
                {
                    return;
                }
                this._socket.SendFile(file.FullName);
                if(this._onSent != null)
                {
                    this._onSent(this, new TelnetEventArgs(TelnetEvent.SentFile, filename));
                }
            }
        }

        public Dictionary<string, string> SendScript(string filename, string scriptVariableIndicator)
        {
            try
            {
                Dictionary<string, string> responses = new Dictionary<string, string>();
                var scriptVariableIndicatorByte = scriptVariableIndicator == null ? Byte.Parse("||") : Convert.ToByte(scriptVariableIndicator.Substring(0, 1).ToCharArray()[0]); ;

                FileInfo file = new FileInfo(filename);
                if (this.ValidateFileExists(file))
                {
                    using (var sr = new StreamReader(file.FullName))
                    {
                        
                        while (sr.Peek() >= 0)
                        {
                            var line = sr.ReadLine();
                            if (line.Contains(scriptVariableIndicator))
                            {
                                var start = line.IndexOf(scriptVariableIndicator);
                                var end = line.LastIndexOf(scriptVariableIndicator)+scriptVariableIndicator.Length;
                                var key = line.Substring(start, end - start);
                                this._keyInputMask = null;
                                if (key.Replace(scriptVariableIndicator, string.Empty).Equals("password", StringComparison.InvariantCultureIgnoreCase))
                                {
                                    this._keyInputMask = "*";
                                }
                                var value = this.SendAndWaitForEnter(line.Replace(key, string.Empty));
                                responses.Add(key.Replace(scriptVariableIndicator, string.Empty), value);
                            }
                            else
                            {
                                this.SendString(line, false);
                            }
                        }
                    }
                }
                return responses;
            }
            catch (Exception ex)
            {
                if (this._onException != null)
                {
                    this._onException(this, new TelnetErrorArgs(ex, string.Format("Error In SendScript [{0}]", ex.Message)));
                }
            }
            return null;
        }

        public void SendANSITemplateString(string template, string message, bool waitForAnyKey = false)
        {
            this.SendString(string.Format(template, message));
            if (waitForAnyKey)
            {
                this.WaitForAnykey();
            }
        }

        public void Pause(int seconds, int multipler = 1000)
        {
            System.Threading.Thread.Sleep(seconds * multipler);
        }

        public bool ValidateFileExists(FileInfo file, ILogger logger = null)
        {
            if (!file.Exists)
            {
                if (logger != null)
                {
                    logger.Error("File Does not Exist [" + file.FullName + "]");
                }
                return false;
            }
            return true;
        }

        void DoSend(string text)
        {
            try
            {
                Byte[] smk = new Byte[text.Length];
                for (int i = 0; i < text.Length; i++)
                {
                    Byte ss = Convert.ToByte(text[i]);
                    smk[i] = ss;
                }

                this._socket.Send(smk, 0, smk.Length, SocketFlags.None);

                if (this._onSent != null)
                {
                    this._onSent(this, new TelnetEventArgs(TelnetEvent.SentData, text));
                }

            }
            catch (Exception ex)
            {
                this.RaiseError(ex, string.Format("DoSend: [{0}]", text));
            }
        }

        void OnRecievedData(IAsyncResult ar)
        {
            // Get The connection socket from the callback
            Socket sock = (Socket)ar.AsyncState;

            if (sock.Connected)
            {
                // Get The data , if any
                int nBytesRec = sock.EndReceive(ar);

                if (nBytesRec > 0)
                {
                    // Decode the received data
                    string received = ANSIColorHelper.CleanInput(Encoding.ASCII.GetString(this._socketBuffer, 0, nBytesRec));

                    // If its a backspace key delete the last character from working and from client terminal input line
                    if (received == ANSICharacterCodes.KeyBackSpace)
                    {
                        if (this._workingData.Length > 0)
                        {
                            this.SendString(ANSICharacterCodes.ANSIBackSpaceKeySequence);
                            this._workingData = this._workingData.Substring(0, this._workingData.Length - 1);
                        }
                        else
                        {
                            this.SendString(ANSICharacterCodes.ANSIMoveForewardOneKeySequence);
                            this._workingData = string.Empty;
                        }
                    }
                    else
                    {
                        this._workingData += received;
                    }

                    // Write out the data
                    if (received.IndexOf("[c") != -1) Negotiate(1);
                    if (received.IndexOf("[6n") != -1) Negotiate(2);

                    // Echo the keyback so the user gets some feedback 
                    this.SendString(this._keyInputMask ?? received);

                    // Send event (if any subscribers)
                    if (this._onReceived != null)
                    {
                        this._onReceived(this, new TelnetEventArgs(TelnetEvent.ReceiveData, received));
                    }

                    // Launch another callback to listen for data
                    AsyncCallback recieveData = new AsyncCallback(OnRecievedData);
                    sock.BeginReceive(this._socketBuffer, 0, this._socketBuffer.Length, SocketFlags.None, recieveData, sock);
                }
                else
                {
                    // If no data was recieved then the connection is probably dead
                    try
                    {
                        sock.Shutdown(SocketShutdown.Both);
                        sock.Close();
                    }
                    finally
                    {
                        if (this._onDisconnected != null)
                        {
                            this._onDisconnected(this, new TelnetEventArgs(TelnetEvent.Disconnect, sock));
                        }
                    }
                }
            }
        }

        void Negotiate(int WhichPart)
        {
            StringBuilder x;
            string neg;
            if (WhichPart == 1)
            {
                x = new StringBuilder();
                x.Append((char)27);
                x.Append((char)91);
                x.Append((char)63);
                x.Append((char)49);
                x.Append((char)59);
                x.Append((char)50);
                x.Append((char)99);
                neg = x.ToString();
            }
            else
            {
                x = new StringBuilder();
                x.Append((char)27);
                x.Append((char)91);
                x.Append((char)50);
                x.Append((char)52);
                x.Append((char)59);
                x.Append((char)56);
                x.Append((char)48);
                x.Append((char)82);
                neg = x.ToString();
            }
            this.SendString(neg, true);
        }

        void RaiseError(Exception ex, string message = null)
        {
            if (this._onException != null)
            {
                this._onException(this, new TelnetErrorArgs(ex, message));
            }
        }

        public string WaitForEnter()
        {
            var result = this.WaitFor(ANSICharacterCodes.KeyCrLf);
            return result == null ? null : result.Replace(ANSICharacterCodes.KeyCrLf, string.Empty);            
        }

        public void WaitForAnykey()
        {
            this.WaitFor(string.Empty, 1);
        }

        public string WaitFor(string waitFor, int? waitForKeyLength = null)
        {
            // Get the starting time
            long lngStart = DateTime.Now.AddSeconds(this.Timeout).Ticks;
            long lngCurTime = 0;
            var position = -1;
            while (position == -1)
            {
                position = this._workingData.ToLower().IndexOf(waitFor.ToLower());

                // Timeout logic
                lngCurTime = DateTime.Now.Ticks;
                if (lngCurTime > lngStart)
                {
                    this.RaiseError(new Exception("Timed Out waiting for : " + waitFor));
                    return null;
                }
                Thread.Sleep(1);
                if(waitForKeyLength.HasValue)
                {
                    if (this._workingData.Length >= waitForKeyLength.Value)
                    {
                        position = 1;
                    }
                }
            }
            var result = this._workingData;
            this._workingData = string.Empty;
            return result;
        }

        public void Dispose()
        {
            try
            {
                this._socket.Close();
            }
            catch (Exception)
            {
                throw;
            }
            finally
            {
                this._socket = null;
            }
        }

    }
}
